#####################################################################
# Product:      KnowledgeEngine (Plone Edition)
#
# File:         SecurityService.py
#
# Copyright:    Copyright (c) 2005, Faulkner Technologies
#
# Author:       Brian Oliver
#
# Description:  Defines an abstract service that is used at the lowest
#               level to determine security on KnowledgeEngine objects,
#               including assessments and repositories.
#
# Terminology:  ACTORS: An Actor is a generic term for either a user or 
#               a group. This concept is introduced as the security 
#               mechanism for both is essentially the same.
#
#               ANONYMOUS ACTOR: An anonymous actor is one whose 
#               identity is "anonymous"
#
#               SECURABLE: A Securable is a generic term for an object
#               that may have security Permissions defined against it.
#               Example 'securables' include; Actors, Repositories,
#               GenericObjects, Components, Reports, Models, Actions,
#               Events, Assessments etc.
#
#               PERMISSION PRECEDENCE: Permissions defined in a group
#               always override the permissions of an individual or
#               sub-group.  Permisions in 'top-level' groups are 
#               thus defined to have 'the highest precendence' when
#               checking and enforcing security.
#
#               EFFECTIVE PERMISSION: An 'effective permission' is a
#               top-level permission.  It is one that has the 
#               highest-precedence over other permissions defined for 
#               a defined securable.
#
#               PERMISSION CHAIN: A 'permission chain' is a linked
#               data structure detailing the precedence of permissions.
#               The first permission in the chain is the 'effective
#               permission', the second permission being the next
#               most 'effective' and so on until the least effective
#               permission.  The chaining of permissions is supported
#               by the Permission class.
#
#               APPLICABLE PERMISSION: A permission is 'applicable'
#               if and only if the current time is within it's 
#               effective from and expires on time frame
#
#               CREDITS: A permission may have 'credits' that specify
#               an arbitary limit on the said permission. eg: the
#               number of times an assessment can be created.  Credits
#               for permissions, securables and actors are managed
#               by the CreditManagementService
#
#               ASSESSMENT SCOPE:  A permission may have an 'assessment scope'
#               that indicates a list of actors (or *) whose assessments
#               the said permission applies.
#
# Rules:        1. 'denied' as precedence over 'granted'.
#
#               eg: If an actor has a 'granted' permission, and the
#               same permission is 'denied' at either a higher-level 
#               (say by a group in which the actor exists or by a 
#               wildcard), the said permission is considered 'denied'.
#
#               eg: If an actor has a 'denied' permission, and the
#               same permission is 'granted' at a higher-level, the
#               permission is 'denied'.
#
#               2. 'granted' has precedence over 'undefined'.
#
#               eg: If an actor has a 'granted' permission, and the
#               said permission is 'not defined' at a higher-level, the
#               said permission is 'granted'
#
#               eg: If a permission is 'not defined' for an actor, but
#               it is 'granted' at a higher-level, the said permission
#               is 'granted'.
#
#               3. Credits for a permission defined on a group are 
#               'shared' between the members of the group.
#
#               eg: If several actors belong to the same group that has a 
#               permission containing y credits, each of the actors in
#               the group share the y credits for the permission.  
#               That is, each actor does not have y individual credits.
#
#               4. A higher-level permission with a 'infinite' credits
#               overrides the credits of lower-level permissions.
#               
#               eg: If a group has infinite credits for a permission,
#               all members of the group have infinite credits, regardless
#               of any specifically defined credit limits.
#
#               5. A higher-level permission with a specific number of
#               credits, overrides lower-level permissions with 'infinite'
#               credits.
#
#               eg: If an actor has 'infinite' credits for a permission,
#               but a group in which they belong has specific credits,
#               the credits of the group take precedence.
#   
#               6. The available credits for a permission is the sum of
#               credits for the SET of defined permissions at all levels.  
#
#               eg: An actor has w credits for a permission.  The actor
#               belongs to groups A and B.  Group A has x credits for
#               the said permission. Group B has y credits for the said 
#               permission.  Group A and group B belong to Group C. 
#               Group C has z credits for the permission.  The total 
#               available credits for the actor is w + x + y + z.
#
#               7. A permission that is 'granted' may effectively be
#               'denied' if the said permission uses 'credits' and
#               there are no 'effective' credits remaining.
#               (this is permission specific)
#
# See Also:     Service, UserProviderService, GroupProviderService
#               Permission, CreditManagementService
#####################################################################

#####################################################################
## KnowledgeEngine Library Imports
from Products.KnowledgeEngine.Core import *
from Products.KnowledgeEngine.Common import *
from Products.KnowledgeEngine.Service import ServiceInfo, Service
from Products.KnowledgeEngine.Model.Model import Model

#####################################################################
## Zope Library Imports
from Products.CMFCore.utils import getToolByName
 
#####################################################################
## Python Library Imports
from time import time
from sets import Set

#####################################################################
## SecurityServiceInfo Class Definition
class SecurityServiceInfo(ServiceInfo):
    """
        Defines the security service meta-information
    """
    
    #---------------------------------------------------------------#
    
    def getClassName(self):
        """
            Returns the name of the class for which 
            we are providing information.
        """
        return 'SecurityService'


    #---------------------------------------------------------------#

    def getDisplayName(self):
        """
            Return a friendly name for the class instances
            (this is used for displaying in lists etc)
        """
    
        return "KnowledgeEngine: Security Service"
        
    #---------------------------------------------------------------#

    def getServiceName(self):
        """
            ABSTRACT: Return the lookup-identity.
            
            This method must be overriden to provide a unique service name
            (this name is used to set the zope id of the service
            when created)
        """
        
        return "knowledgeengine_securityservice"

#####################################################################
## SecurityService Class Definition
class SecurityService(Service):
    """
        Defines a tool that may be used to manage Knowledge Engine
        security
    """

    #---------------------------------------------------------------#

    #ABSTRACT TRANSIENT: a ServiceInfo implementation instance to provide info about this service
    _SERVICEINFO = SecurityServiceInfo()

    #---------------------------------------------------------------#

    def __init__(self):
        """
            Constructor for the object
        """
        
        #this is a stateless mix-in
        pass
        
    #---------------------------------------------------------------#
        
    def granted(self, permissionIdentity, securableIdentity, actorIdentity):
        """
            FINAL: Returns the a permission with the specified permissionIdentity, securableIdentity
            (or the wildcard '*') and actor (including the wildcard '*' and the 'anonymous' actor)
            is granted.

            NOTE: Uses getEffectivePermission(...) so determine if granted

            RETURN TYPE: Boolean {true if the permission is granted, false if it is denied or undefined)            
        """            
    
        #find the effective permission
        permission = self.getEffectivePermission(permissionIdentity, securableIdentity, actorIdentity)
        
        #was it defined?
        if permission is None:
            return false
        else:
            return permission.granted()
        
    #---------------------------------------------------------------#
        
    def denied(self, permissionIdentity, securableIdentity, actorIdentity):
        """
            FINAL: Returns the a permission with the specified permissionIdentity, securableIdentity
            (or the wildcard '*') and actor (including the wildcard '*' and the 'anonymous' actor)
            is denied.

            NOTE: Basically proxied onto the granted(...) method

            RETURN TYPE: Boolean {true if the permission is denied or undefined, false if granted)            
        """            
        
        return not self.granted(permissionIdentity, securableIdentity, actorIdentity)
        
    #---------------------------------------------------------------#
        
    def getEffectivePermission(self, permissionIdentity, securableIdentity, actorIdentity):
        """
            FINAL: Returns the Permission and associated Permission chain for the
            provided arguments.
            
            This method is the PRIMARY means of checking permissions.  It returns
            the entire applicable permission chain as well as the most specific 
            permission so that things like assessment scope (sets) can be derived
            (which is the union of all scopes for a permissionIdentity)

            RETURN TYPE: Permission (or None if undefined)
        """            

        #we need to know all of the groups for the actor so we can build
        #a complete 'effectiveSet' of permissions for permissionIdentity
        #(initially we assume there are no groups for the actor)
        if actorIdentity == '*' or actorIdentity == 'anonymous':
            groups = []
        else:            
            groups = getGroupProviderService(self).getGroupsForActor(actorIdentity) 
            
        #an initial 'effectiveSet'
        effectiveSet = []

        #the most specific permissions are for explicitly defined securables and actors        
        if securableIdentity != '*' and actorIdentity != '*':            
            #add the most specific permission for the actor to the effective set
            effectiveSet = effectiveSet + [(permissionIdentity, securableIdentity, actorIdentity)]
            
            #add the most specific permissions for the actors groups to the effective set
            effectiveSet = effectiveSet + [(permissionIdentity, securableIdentity, group.getIdentity()) for group in groups]
        
        #the next most specific permissions are for explicitly defined actors
        if actorIdentity != '*':            
            #add the a general permission for the securable but be specific for the actor 
            effectiveSet = effectiveSet + [(permissionIdentity, '*', actorIdentity)]
            
            #add the a general permission for the securable but be specific for the actor's groups
            effectiveSet = effectiveSet + [(permissionIdentity, '*', group.getIdentity()) for group in groups]
        
        #the next most specific permissions are for all actors (except the anonymous actor)
        if securableIdentity != '*' and actorIdentity != 'anonymous':            
            #add the a general permission for the securable but be specific for the actor 
            effectiveSet = effectiveSet + [(permissionIdentity, securableIdentity, '*')]

        #the next most general permission are is for all securables and for all actors (except the anonymous actor)
        if actorIdentity != 'anonymous':            
            #add the a general permission for the securable but be specific for the actor 
            effectiveSet = effectiveSet + [(permissionIdentity, '*', '*')]
        
        #now construct the a permission chain for permissions that exist in the effective set
        firstPermission = None
        lastPermission = None
        
        for (aPermissionIdentity, aSecurableIdentity, aActorIdentity) in effectiveSet:
        
            #get the actual permission for the specified set
            permission = self.getSpecificPermission(aPermissionIdentity, aSecurableIdentity, aActorIdentity)
            
            #does the permission exists? (it may not be defined, in which case we just move onto the next permission)
            #is it applicable? (if it's not applicable, we assume it doesn't exist)
            if permission is not None and permission.getIsApplicable():
                
                #add granted permissions to the chain
                if permission.granted():                
                    #is it the first permission in the chain?
                    if firstPermission is None:
                        firstPermission = permission
                        lastPermission = permission
                    #add to the end of the permission chain                        
                    else:
                        lastPermission.addLessEffectivePermission(permission)                        
                        lastPermission = permission
                        
                #found a denied permission!
                else:
                    #denied permissions override granted permissions                        
                    return permission
        
        #the first permission is the most specific one we found for the permission requested
        return firstPermission
        
    #---------------------------------------------------------------#

    def getSpecificPermission(self, permissionIdentity, securableIdentity, actorIdentity):
        """
            ABSTRACT: Returns the Permission with the specified
            permissionIdentity, securableIdentity and actorIdentity.
            
            Returns None if the permission doesn't exist.
            
            NOTE: This method is actually to support 
            getEffectiveSpecificPermission(...).  It should not be called
            by external code.
            
            RETURN TYPE: Permission
        """            

        #by default there are no permission for an actor
        return None
        
    #---------------------------------------------------------------#

    def getPermissionsForSecurable(self, permissionIdentity, securableIdentity):
        """
            ABSTRACT: Returns the list of Permissions for all actors
            with the specified permissionIdentity and specific securableIdentity
            
            NOTE: If you require the permissions for the generic/wildcard securable
            you need to call this method explicitly with a securable of '*'
            
            NOTE: This method is primarily use to locate the permissions
            defined on securables so they can be rendered and managed through 
            a user-interface
            
            RETURN TYPE: [Permission]
        """            

        #by default there are no permissions
        return []
    
    #---------------------------------------------------------------#

    def getModels(self):
        """
            FINAL: Returns a list of the Models in the portal
        """
        
        #get the portal catalog
        catalog = getToolByName(self, "portal_catalog")
        
        #search for Models using the meta_type
        searchResults = catalog(meta_type = Model.meta_type)
        
        #results aren't the actual objects, so lets reference them
        models = []
        for searchResult in searchResults:
            #attempt to resolve the actual object reference
            model = searchResult.getObject()
            
            #only add models that truely exist!
            if model is not None:
                models = models + [model]
                
        return models

    #---------------------------------------------------------------#

    def getModelsByIdentity(self, modelidentitylist):
        """
            ABSTRACT: Returns a list of the Models given a list of model identities
            
            NOTE: This method is called from myknowledge, particularly from customized
            versions of myknowledge that might be used to only list assessments from 
            particular models.
        """

        #get complete list of models
        allmodels = self.getModels()

        if modelidentitylist==[]:
            return allmodels

        orderedmodels=[]
        for model in allmodels:
            if model.getIdentity() in modelidentitylist:
                orderedmodels.append(model)

        return orderedmodels

    #---------------------------------------------------------------#

    def savePermission(self, permission):
        """
            ABSTRACT: Adds/updates the specified permission to the security service.
            
            NOTE: does not store any lower-level permissions if
            the said permission is part of a permission chain.
            
            If the permission already exists, it is overwritten 
            by the provided permission.
        """

        pass        
        
    #---------------------------------------------------------------#

    def removePermission(self, permission):
        """
            ABSTRACT: Removes the specified permission from the security service
            
            NOTE: does not remove any lower-level permissions if
            the said permission is part of a permission chain.
        """

        pass

    #---------------------------------------------------------------#

    def getAccumulatedAssessmentScope(self, permission):
        """
            FINAL: Returns a list of accumulated actorIdentities 
            whose assessment are considered 'in scope' for the
            specified permission (chain).
            
            RETURN TYPE: [String]
        """
    
        #build a set of all of the explicitly specified actors in the permission chain
        actorIdentities = Set(permission.getAssessmentScope())
        for aPermission in permission.getLessEffectivePermissions():
            actorIdentities.update(aPermission.getAssessmentScope())

        #is universal scope '*' specified?
        if '*' in actorIdentities:
            #if a universal scope is specified, we don't need to do anything but return universal!
            return ['*']
            
        #must be a specific scope            
        else:            
            #get the group provider service
            groupProviderService = getGroupProviderService(self)

            #build a complete set of all groups
            resultSet = Set()
            for actorIdentity in actorIdentities:
                #process the current actor iff it's not in the result
                if not actorIdentity in resultSet:
                    #make sure the current actor is in the result
                    resultSet.add(actorIdentity)

                    #get and all of the groups for the actor
                    resultSet.update(groupProviderService.getGroupsForActor(actorIdentity))

            #return the result as a list
            return [actorIdentity for actorIdentity in resultSet]

    #---------------------------------------------------------------#

    def getAssessmentScopeDescription(self, assessmentScope):
        """
            FINAL: Returns a description of the accumulated 
            assessment scope for the specified assessmentScope ([String (actorIdentities)]).
            
            This is used to describe scope to end users in 
            the permission form
            
            RETURN TYPE: String
        """
        #get the group provider service
        groupProviderService = getGroupProviderService(self)
        
        #translate the scope into english
        result = ""
        for actorIdentity in assessmentScope:
           
            if actorIdentity == '*':
                scope = "All Assessments"
            elif actorIdentity == 'anonymous':
                scope = "Anonymous User Assessments"
            elif actorIdentity == 'authenticateduser':
                scope = "Authenticated User Assessments"
            elif actorIdentity == 'authenticatedusergroup':
                scope = "Assessments for members of the Authenticated User's Group(s)"
            elif actorIdentity == 'authenticatedusermanagedgroup':
                scope = "Assessments for members of the Authenticated User's Managed Group(s)"
            else:
                group = groupProviderService.getGroup(actorIdentity)                
                if group is not None:
                    scope = "Assessments for %s group" % group.getFullname()                
                else:
                    scope = "(unknown:%s)" % group
                    
            if len(result) > 0:
                result = result + ", " + scope
            else:
                result = scope
                
        return result
        
    #---------------------------------------------------------------#

#####################################################################
## Class Initialisation
# (none)
